﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace FerrScript
{
    /// <summary>
    /// A Singleton class that compiles FerrScript scripts. It automatically finds active assemblies
    /// for use in creating objects within FerrScript.
    /// </summary>
    public class FerrScriptCompiler
    {
        public delegate void FerrScriptRuntimeErrorEvent(string aExceptionMessage);

        #region Singleton
        private static FerrScriptCompiler instance;
        public  static FerrScriptCompiler Inst
        {
            get { if (instance == null) instance = new FerrScriptCompiler(); return instance; }
        }
        #endregion

        #region Fields
        private  List<ScriptFunction> functions;
        private  List<ScriptVariable> variables;
        private  List<string> errors;
        private  int scriptCount;
        #endregion

        #region Properties
        /// <summary>
        /// A list of error descriptions gathered from the last compile operation
        /// </summary>
        public List<string> Errors
        {
            get { return errors; }
        }
        #endregion

        #region Events
        /// <summary>
        /// This event gets called every time a script trips a runtime error. This doesn't include any
        /// errors that .NET throws itself.
        /// </summary>
        public   event FerrScriptRuntimeErrorEvent OnRuntimeError;
        internal void  ThrowException(string aExceptionText)
        {
            if (OnRuntimeError != null)
                OnRuntimeError(aExceptionText);
        }
        #endregion

        #region Constructor
        private FerrScriptCompiler()
        {
            functions    = new List<ScriptFunction>();
            variables    = new List<ScriptVariable>();
            errors       = new List<string>();

            Initialize();
        }
        #endregion

        #region Initialization Methods
        private void Initialize   ()
        {
            functions = new List<ScriptFunction>();
            functions.Add(new ScriptFunction("+", BuiltinFunctions.Add     ));
            functions.Add(new ScriptFunction("-", BuiltinFunctions.Subtract));
            functions.Add(new ScriptFunction("*", BuiltinFunctions.Multiply));
            functions.Add(new ScriptFunction("/", BuiltinFunctions.Divide  ));

            functions.Add(new ScriptFunction(">",  BuiltinFunctions.LargerThan));
            functions.Add(new ScriptFunction("<",  BuiltinFunctions.LessThan));
            functions.Add(new ScriptFunction(">=", BuiltinFunctions.LargerThanEq));
            functions.Add(new ScriptFunction("<=", BuiltinFunctions.LessThanEq));
            functions.Add(new ScriptFunction("==", BuiltinFunctions.EqualTo));
            functions.Add(new ScriptFunction("!=", BuiltinFunctions.NotEqualTo));
            functions.Add(new ScriptFunction("&&", BuiltinFunctions.And));
            functions.Add(new ScriptFunction("||", BuiltinFunctions.Or));

            functions.Add(new ScriptFunction("sqrt", BuiltinFunctions.Sqrt));
            functions.Add(new ScriptFunction("sin",  BuiltinFunctions.Sin));
            functions.Add(new ScriptFunction("cos",  BuiltinFunctions.Cos));
            functions.Add(new ScriptFunction("pow",  BuiltinFunctions.Pow));

            functions.Add(new ScriptFunction("if",    BuiltinFunctions.If));
            functions.Add(new ScriptFunction("body",  BuiltinFunctions.Body));
            functions.Add(new ScriptFunction("while", BuiltinFunctions.While));
            functions.Add(new ScriptFunction("for",   BuiltinFunctions.For));

            functions.Add(new ScriptFunction("set",  BuiltinFunctions.Set));
            functions.Add(new ScriptFunction("get",  BuiltinFunctions.Get));
            functions.Add(new ScriptFunction("new",  BuiltinFunctions.New));
            functions.Add(new ScriptFunction("call", BuiltinFunctions.Call));

            functions.Add(new ScriptFunction("print", BuiltinFunctions.Print));
            functions.Add(new ScriptFunction("rand",  BuiltinFunctions.Rand));
        }
        /// <summary>
        /// Adds a native function to the compile list. These functions are faster than (call)ed functions, 
        /// but still slower than CompilerInstruction functions. This method will throw exceptions if you 
        /// try calling it -after- you've compiled any scripts.
        /// </summary>
        /// <param name="aName">Name of the function from within FerrScript</param>
        /// <param name="aClass">The object that this function calls from</param>
        /// <param name="aMethodName">The name of the method within the class</param>
        public void AddFunction(string aName, object aClass, string aMethodName)
        {
            if (scriptCount > 0)
                throw new Exception("You're trying to add a function -after- scripts have started compiling!");

            functions.Add(new ScriptFunction(aName, aClass, aMethodName));
        }
        /// <summary>
        /// Adds a native function to the compile list. These functions are faster than (call)ed functions, 
        /// but still slower than CompilerInstruction functions. This method will throw exceptions if you 
        /// try calling it -after- you've compiled any scripts.
        /// </summary>
        /// <param name="aName">Name of the function from within FerrScript</param>
        /// <param name="aClassType">The class that this function calls from</param>
        /// <param name="aMethodName">The name of the method within the class</param>
        public void AddFunction(string aName, Type aClassType, string aMethodName)
        {
            if (scriptCount > 0)
                throw new Exception("You're trying to add a function -after- scripts have started compiling!");

            functions.Add(new ScriptFunction(aName, aClassType, aMethodName));
        }
        #endregion

        #region Compiling Methods
        /// <summary>
        /// This compiles a FerrScript script into a set of instructions for faster execution. Errors
        /// generated here will be stored in the 'errors' field
        /// </summary>
        /// <param name="aScript">The script to compile, as text, not as a filename</param>
        /// <returns>A compiled script object</returns>
        public  FScript      Compile       (string aScript)
        {
            List<ScriptVariable>      localVariables   = new List<ScriptVariable>     ();
            List<CompiledFunction>    localFunctions   = new List<CompiledFunction>   ();
            List<CompilerInstruction> rootInstructions = new List<CompilerInstruction>();
            List<string>              functionNames    = new List<string>             ();
            List<CompilerInstruction> functionCalls    = new List<CompilerInstruction>();

            // keep track of how many scripts we've compiled
            scriptCount++;

            // clear out any previous errors
            errors.Clear();

            // parse the script
            ParseElement pe = Parser.ParseScript(aScript);
            if (pe == null)
            {
                for (int i = 0; i < Parser.parseErrors.Count; i++)
                    errors.Add(Parser.parseErrors[i]);
                return null;
            }

            // compile the script
            CompilerInstruction ins = new CompilerInstruction();
            CompileElement(pe, ins, localVariables, functionNames, functionCalls, 0);

            // check to make sure our script has stuff in it at this point
            if (ins == null || ins.instructions == null)
            {
                errors.Add("No instructions were found!");
                return null;
            }

            // transfer the variables to something theoretically faster
            ScriptVariable[] varArr = localVariables.ToArray<ScriptVariable>();

            // do rawr, or like.. something awesome, aka picking out functions from the root instruction
            for (int i = 0; i < ins.instructions.Length; i++)
            {
                if (ins.instructions[i].value.type == ScriptValueType.Function)
                {
                    if (ins.instructions[i].instructions != null)
                    {
                        ins.value.type = ScriptValueType.Delegate;
                        ins.value.value = (object)((ScriptFunctionDelegate)BuiltinFunctions.Body);
                        localFunctions.Add(new CompiledFunction((string)ins.instructions[i].value.value, ins.instructions[i]));
                    }
                }
                else
                {
                    rootInstructions.Add(ins.instructions[i]);
                }
            }
            ins.instructions = rootInstructions.ToArray();
            

            // update the function calls
            for (int i = 0; i < functionCalls.Count; i++)
            {
                for (int f = 0; f < localFunctions.Count; f++)
                {
                    if ((string)functionCalls[i].value.value == localFunctions[f].name)
                    {
                        functionCalls[i].value.value = localFunctions[f];
                        break;
                    }
                }
            }

            // check to see if we've got any additional errors here
            if (errors.Count > 0)
                return null;

            // and construct the script!
            if (ins != null)
            {
                FScript script = new FScript(aScript, ins, varArr, localFunctions.ToArray());
                return script;
            }
            else
            {
                errors.Add("No instructions found in the compiled script!");
                return null;
            }
        }
        private void        CompileElement(ParseElement         aElement,       CompilerInstruction  aIns,           List<ScriptVariable> aLocalVariables, List<string> aFunctionNames, List<CompilerInstruction> aFunctionCalls, int aCurrIndent)
        {
            aIns.value = GetScriptValue(aElement, aLocalVariables, aFunctionNames, aCurrIndent);
            if (aIns.value.type == ScriptValueType.FunctionCall)
                aFunctionCalls.Add(aIns);

            if (aElement.subElements != null && aElement.subElements.Count > 0)
            {
                aIns.instructions = new CompilerInstruction[aElement.subElements.Count];
                for (int i = 0; i < aElement.subElements.Count; i++)
                {
                    aIns.instructions[i] = new CompilerInstruction();
                    CompileElement(aElement.subElements[i], aIns.instructions[i], aLocalVariables, aFunctionNames, aFunctionCalls, aCurrIndent + 1);
                    KillVariables(aLocalVariables, aCurrIndent);
                }
            }
        }
        private ScriptValue GetScriptValue(ParseElement         aElement,       List<ScriptVariable> localVariables, List<string>         aFunctionNames,  int          aCurrIndent)
        {
            ScriptValue val = new ScriptValue(0);

            if (aElement.value == "defun")
            {
                val.type  = ScriptValueType.Function;
                if (aElement.subElements.Count <= 0)
                    errors.Add("Function has no name!");
                else
                {
                    aFunctionNames.Add(aElement.subElements[0].value);

                    val.value = aElement.subElements[0].value;
                    aElement.subElements.RemoveAt(0);
                }
            }
            else
            {
                if (aElement.IsAtom())
                {
                    float  fRet=0;
                    int    iRet=0;

                    bool isInt = false;
                    bool isFloat = false;

                    // try parsing it as a float or an int, TryParse doesn't work on Compact framework
                    try { iRet = int.Parse(aElement.value); isInt = true; }
                    catch { try { fRet = float.Parse(aElement.value); isFloat = true; } catch { } }

                    if (isInt)
                    {
                        val.type  = ScriptValueType.Number;
                        val.value = iRet;
                    }
                    else if (isFloat)
                    {
                        val.type  = ScriptValueType.Number;
                        val.value = fRet;
                    }
                    else if (aElement.value.StartsWith("\'") && aElement.value.EndsWith("\'"))
                    {
                        val.type  = ScriptValueType.String;
                        val.value = aElement.value.Replace("\'", "");
                    }
                    else if (aElement.value.StartsWith("~"))
                    {
                        string typeName = aElement.value.Remove(0, 1);
                        val.type        = ScriptValueType.Type;
                        val.value       = Type.GetType(typeName);

                        Assembly ass = null;
                        string[] words = typeName.Split('.');
                        try
                        {
                            ass = Assembly.Load(words[0]);
                        }
                        catch
                        {
                            errors.Add("Unable to find namespace " + words[0] + " for " + aElement.value);
                        }

                        if (ass != null)
                            val.value = ass.GetType(typeName);

                        // if we still didn't find it, put in a compile error
                        if (val.value == null)
                            errors.Add("Variable type not found!: " + aElement.value);
                    }
                    else
                    { // variable, field, function, or method
                        if (aElement.value.Contains('.'))
                        {
                            errors.Add("The '.' operator is not supported in this context - " + aElement.value);
                        }
                        else
                        {
                            // if it's not a function
                            if (!GetFunction(aElement.value, ref val))
                            {
                                if (!LocalFunction(aElement.value, aFunctionNames, ref val))
                                {
                                    // check a variable
                                    if (aElement.subElements == null || aElement.subElements.Count <= 0)
                                    {
                                        if (!GetVariable(aElement.value, localVariables, ref val))
                                            MakeVariable(aElement.value, localVariables, ref val, aCurrIndent - 2);
                                    }
                                    else
                                    {
                                        errors.Add("No function found by the name of " + aElement.value + "!");
                                    }
                                }
                            }
                        }
                    }
                }
                else
                { // function or method

                    // method
                    if (aElement.value.Contains('.'))
                    {
                        errors.Add("The '.' operator is not supported in this context - " + aElement.value);
                    }
                    // function
                    else
                    {
                        if (!GetFunction(aElement.value, ref val))
                            if (!LocalFunction(aElement.value, aFunctionNames, ref val))
                                errors.Add("Could not find function '" + aElement.value + "'");
                    }
                }
            }

            return val;
        }
        private static bool        LocalFunction (string               aName,          List<string>         aFunctionNames, ref ScriptValue      val)
        {
            for (int i = 0; i < aFunctionNames.Count; i++)
            {
                if (aFunctionNames[i] == aName)
                {
                    val.type  = ScriptValueType.FunctionCall;
                    val.value = aName;
                    return true;
                }
            }
            return false;
        }
        private static void        MakeVariable  (string               aName,          List<ScriptVariable> localVariables, ref ScriptValue      val,             int          aCurrIndent)
        {
            ScriptVariable sv = new ScriptVariable();
            sv.varName = aName;
            sv.indent  = aCurrIndent;
            sv.alive   = true;

            val.type  = ScriptValueType.Variable;
            val.value = sv;

            localVariables.Add(sv);
        }
        private static bool        GetVariable   (string               aName,          List<ScriptVariable> localVariables, ref ScriptValue      val)
        {
            for (int i = 0; i < localVariables.Count; i++)
            {
                if (localVariables[i].varName == aName && localVariables[i].alive == true)
                {
                    val.type = ScriptValueType.Variable;
                    val.value = localVariables[i];
                    return true;
                }
            }
            return false;
        }
        private void        KillVariables (List<ScriptVariable> localVariables, int                  aCurrIndent)
        {
            for (int i = 0; i < localVariables.Count; i++)
            {
                if (aCurrIndent < localVariables[i].indent)
                    localVariables[i].alive = false;
            }
        }
        private bool        GetFunction   (string               aFunctionName,  ref ScriptValue      val)
        {
            for (int i = 0; i < functions.Count; i++)
            {
                if (functions[i].functionName == aFunctionName)
                {
                    val.type  = ScriptValueType.Delegate;
                    val.value = (ScriptFunctionDelegate)functions[i].Call;
                    return true;
                }
            }
            return false;
        }
        #endregion
    }
}
